package com.hx.haowumall.haowumallproduct.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.hx.common.utils.Query;
import com.hx.haowumall.haowumallproduct.service.CategoryBrandRelationService;
import com.hx.haowumall.haowumallproduct.vo.Catelog2Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hx.common.utils.PageUtils;

import com.hx.haowumall.haowumallproduct.dao.CategoryDao;
import com.hx.haowumall.haowumallproduct.entity.CategoryEntity;
import com.hx.haowumall.haowumallproduct.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        //1、查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        //2、组装父子的树形结构

        //2.1)\找到所有的一级分类//???
        List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity ->
                categoryEntity.getParentCid() == 0).map((menu)-> {
            menu.setChildren(getChildrens(menu,entities));
            return menu;
        }).sorted((menu1,menu2)->{
            return  menu1.getSort() - menu2.getSort();
        }).collect(Collectors.toList());

        return level1Menus;
    }

    @Override
    public void removeMenusByIds(List<Long> asList) {
        //TODO
        //1\检查当前删除的菜单，是否被别的地方引用
        baseMapper.deleteBatchIds(asList);
    }



    //递归查找所有菜单的子菜单
    private  List<CategoryEntity> getChildrens (CategoryEntity root,List<CategoryEntity> all){
        List<CategoryEntity> children = all.stream().filter(item -> {
            return item.getParentCid() == root.getCatId();
        }).map(item -> {
            //1找到子菜单
            item.setChildren(getChildrens(item, all));
            return item;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() ==null ? 0:menu1.getSort())- (menu2.getSort()==null?0:menu2.getSort());
        }).collect(Collectors.toList());

        return children;

    }
    // 找父母
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        Boolean tick = true;
        ArrayList<Long> paths = new ArrayList<>();
        CategoryEntity byId = this.getById(catelogId);
        paths.add(catelogId);
        while (tick){
            if(byId.getParentCid() !=0 ){
                paths.add(byId.getParentCid());
                byId = this.getById(byId.getParentCid());
                tick = true;
            }
            else {
                tick = false;
            }

        }
        Collections.reverse(paths);

        return paths.toArray(new Long[paths.size()]);

        /*List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId,paths);

        return parentPath.toArray(new Long[parentPath.size()]);*/
    }

    private List<Long> findParentPath(Long catelogId,List<Long> paths){
        CategoryEntity byId = this.getById(catelogId);
        if (byId.getParentCid() != 0){
            findParentPath(byId.getParentCid(),paths);
        }
        paths.add(catelogId);

        return paths;
    }

    /**
     * @CacheEvict:缓存失效模式
     * @Caching：同时进行多种操作
     * @Caching(evict = {
     *             @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
     *             @CacheEvict(value = "category",key = "'getCatelogJson'")
     *     })
     *全部清除category分区的数据:@CacheEvict(value = "category",allEntries = true )
     * @CachePut双写模式
     *
     * 级联更新所有关联的数据
     * */

    @CacheEvict(value = "category",allEntries = true )
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());

    }

    /**
     * 1、每一个需要缓存的数据都要指定放到那个名字下的缓存（缓存的分区-按照业务类型分区）
     *     @Cacheable("{category}")
     * 2、代表当前的方法需要缓存，如果缓存中有，方法不用调用。如果缓存中没有就调用该方法，最后将方法结果放入缓存
     * 3、默认行为
     *      1)、如果缓存中有，方法不用调用。
     *      2)、hey默认自动生成：缓存的名字：:Stmplekey[]（自主生咸的Key值）
     *      3)、缓存的valwe的值。默认使用jan序列化机制，将序列化后的数据存到redia
     *      4)、默认eel时间﹣21
     * 4、自定义：
     *      1）、指定生成的缓存使用的Mey key属性指定，接受一个spEl（表达式）//详情见spring官网
     *      2)、指定缓存的数据的存活时间
     *      3)、将数据保存为json格式
     *5、Spring-Cache的不足：
     *      1）、读模式：
     *          缓存穿透：查询一个null数据。解决：缓存空数据：ache-null-values=true
     *          缓存击穿：大量并发进来同时查询一个正好过期的数据。解决：（默认不加锁）加锁：sync = true(本地同步锁)
     *          缓存雪崩，大量的hey同时过期。解决，加随机时间。加上过期时间。:spring.cache.redis.time-to-live=3600000
     *      2)、写模式：（缓存与数据库一致）
     *          1）、读写加锁
     *          2)、引入Canal，感知到MySQL的更新去更新数据库
     *          3)、读多写多，直接去数据库查询
     *
     *  原理：
     *      CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache
     *  总结：
     *      常规数据（读多写少，即时性，一致性要求不高的数据）；完全可以使用Spring-Cache：写模式（只要缓存特殊数据有过期时间就足够了）
     *      特殊设计理：CacheManager
     *
     * */

    @Cacheable(value = {"category"},key = "#root.method.name",sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {

        long l = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        System.out.println("消耗时间:"+(System.currentTimeMillis() - l));

        return categoryEntities;
    }


    @Cacheable(value = {"category"},key = "#root.method.name")
    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson(){

        System.out.println("》》查询了数据库");
        /*
         * 1、将数库的多次查询变为一次
         * */

        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1、查出所有1分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

        //2、封装数据
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //11、查出每一个一级分类的所有的二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

            //22、封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo =
                            new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //111、查出每一个二级分类的所有的三级分类
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
                    if (level3Catelog != null) {
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //222、封装上成指定的格式
                            Catelog2Vo.Catelog3Vo catelog3Vo =
                                    new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());

                            return catelog3Vo;
                        }).collect(Collectors.toList());

                        catelog2Vo.setCatalog3List(collect);
                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }

            return catelog2Vos;
        }));

        return parent_cid;

    }


    //TODO 产生堆外内存溢出: OutOfDirectMemoryError
    //1)、springboot2.0以后默认使用Lettuce作为操作redis的客户端。它使用netty进行网络通信。
    //2)、Lettuce的ug导致netty堆外内存溢出 -Xmx300m; netty如果没有指定堆外内存，默认使用﹣Xmx300m
    //可以通过﹣Dio.netty.maxDirectMemory进行设置
    //解决方案：不能使用﹣Dio.netty.maxDirectMemory只去调大堆外内存。
    //1)、升级Lettuce客户端。 2)、切换使用jedis
    //redisTemlate：
    //lettuce、jedis操作redis的底层客户端》spring再次封装redisTemlate

//    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson2() {
        /*给缓存中放入json字符串，拿出的json字符串，还能逆转为能用的对象类型：【序列化与反序列化】
        * */

        /*
        * 1、空结果缓存：解决缓存穿透
        * 2、设置过期时间（加上随机值），解决缓存雪崩
        * 3、加锁：解决缓存击穿
        * */

        //缓存》》1、加入缓存逻辑,存入的数据都为json字符串(跨语言、跨平台)
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)){
            //缓存》》2、缓存中没有，查询数据库
            System.out.println("缓存没有该数据》》查询数据库》》");
            Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDbWithRedisLock();

            return catelogJsonFromDb;
        }

        System.out.println("缓存有该数据》》直接返回》》");
        //转为本地对象类型
        Map<String, List<Catelog2Vo>> result =
                JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        return result;
    }

    /**
     * 缓存数据一致性的问题
     * 1、双写模式
     * 2、失效模式（本项目选择）：1）、缓存的所有数据都有过期时间，数据过期下一次查询触发主动更新
     *                       2）、读写数据的时候加上分布式的读写锁
     * */

    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedissonLock() {

        //1、占分布式锁。锁的粒度的越细越快
        RLock lock = redisson.getLock("catelogJson-lock");
        lock.lock();

        System.out.println("获取分布式锁成功。。。");
        Map<String, List<Catelog2Vo>> dataFromDb;
        try {
            dataFromDb = getDataFromDb();
        } finally {
            lock.unlock();
        }

        return dataFromDb;
    }


    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock() {

        //锁》》1、占封闭式锁。去redis占坑
        String uuid = UUID.randomUUID().toString();
        //加锁成功。。执行业务
        //锁》》2、设置锁的过期时间,必须是同步的是原子操作  redisTemplate.expire("lock",30,TimeUnit.SECONDS);
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,30,TimeUnit.SECONDS);

        if (lock){
            System.out.println("获取分布式锁成功。。。");
            Map<String, List<Catelog2Vo>> dataFromDb;
            try{
                dataFromDb = getDataFromDb();
            }finally {
                //删除锁 redisTemplate.delete("lock");(获取值对比+对比成功删除锁也必须为原子操作 lua脚本实现解锁)
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if (uuid.equals(lockValue)){
//                //删除自己的锁
//                redisTemplate.delete("lock");
//            }

                //原子删除锁
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                        Arrays.asList("lock"), uuid);
            }

            return dataFromDb;
        }else {
            //加锁失败。。重试（100ms）
            System.out.println("获取分布式锁失败。。。等待重试");
            try {
                Thread.sleep(100);
            }catch (Exception e){

            }
            return getCatelogJsonFromDbWithRedisLock();
        }

    }

    //查询数据库
    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        //得到锁后再验证一次缓存中有没有数据，如果没有才继续查询
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            //缓存不为空直接返回
            Map<String, List<Catelog2Vo>> result =
                    JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
                    });
            return result;
        }

        System.out.println("》》查询了数据库");
        /*
         * 1、将数库的多次查询变为一次
         * */

        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //1、查出所有1分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

        //2、封装数据
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //11、查出每一个一级分类的所有的二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

            //22、封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo =
                            new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //111、查出每一个二级分类的所有的三级分类
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
                    if (level3Catelog != null) {
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //222、封装上成指定的格式
                            Catelog2Vo.Catelog3Vo catelog3Vo =
                                    new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());

                            return catelog3Vo;
                        }).collect(Collectors.toList());

                        catelog2Vo.setCatalog3List(collect);
                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }

            return catelog2Vos;
        }));

        //缓存》》3、查到的数据放入缓存,将对象转为json放在缓存中
        String jsonString = JSON.toJSONString(parent_cid);
        redisTemplate.opsForValue().set("catalogJSON", jsonString, 1, TimeUnit.DAYS);
        return parent_cid;
    }


    //从数据库查询并封装分类数据库
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithLocalLock() {

//        //一、如果缓存中有数据就用缓存中的数据
//        Map<String, List<Catelog2Vo>> catalogJson = (Map<String, List<Catelog2Vo>>) cahe.get("catelogJson");
//        if (cahe.get("catelogJson") == null){
//
//            cahe.put("catelogJson",parent_cid);
//        }
//        return  catalogJson;

        //只要是同一把锁就可以锁住本地的所有线程
        //1、synchronized (this)：springboot所有的组件在容器都是单例的
        synchronized (this){
            //得到锁后再验证一次缓存中有没有数据，如果没有才继续查询
            return getDataFromDb();
        }


    }

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {

        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid() == parent_cid).collect(Collectors.toList());

        //return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
        return collect;
    }

}